package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Slf4j
@Component
/*
redis相关工具类
 */
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    //构造方法
    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    //将key-value存入redis（直接向redis设置过期时间）
    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    //将key-value存入redis（逻辑过期）
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plusMinutes(unit.toSeconds(time)));
        redisData.setData(value);
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }


    /**
     * 利用空值解决缓存穿透
     * @param keyPrefix    查询redis数据库key的前缀
     * @param id        查询数据库所用的id
     * @param type      返回对象的类型
     * @param dbFallBack    用户查询数据库的方法，ID为查询所用的字段，R为返回对象类型
     * @param time      存入redis的过期时间
     * @param unit      存入redis过期时间的类型
     * @return
     * @param <R>   返回对象类型
     * @param <ID>  当前数据的id
     */
    public <R, ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallBack, Long time, TimeUnit unit
    ){
        //构造redis中的key
        String key = keyPrefix + id;

        //通过查询缓存判断是否存在商品
        String json = stringRedisTemplate.opsForValue().get(key);

        //判断商品是否存在，不存在则查数据库并写入缓存
        //商品信息存在，反序列化为shop对象后返回
        if (StrUtil.isNotBlank(json)){  //只有当字符串有内容时才为真
            R r = JSONUtil.toBean(json, type);
            return r;
        }
        //商品信息缓存中不存在，查询数据库并写回redis，返回
        //判断缓存命中的是否为空值
        if (json != null){
            return null;
        }
        //查询数据库
        R r = dbFallBack.apply(id);
        if (r == null) {
            //将空值写入redis，设置有效期为短有效期
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, unit);
            return null;
        }
        this.set(key, r, time, unit); //将shop序列化为json存储
        return r;
    }



    //用于缓存重建的缓存池
    private static final ExecutorService CACHE_REBUILD_EXCUTOR = Executors.newFixedThreadPool(10);

    //通过setnx尝试获取锁
    //若对应锁的key不存在，则返回值为true，代表可以进行获取
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    //释放锁
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

    /**
     * 缓存击穿逻辑过期解决方案
     * @param keyPrefix     key前缀
     * @param id        查询用的id
     * @param type      调用者需要返回对象的类型
     * @param dbFallBack        查询数据库的方法
     * @param time      逻辑过期时间
     * @param unit      逻辑过期指定的时间类型
     * @return
     * @param <R>       返回对象的参数类型
     * @param <ID>      id的参数类型
     */
    public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallBack, Long time, TimeUnit unit){
        //构造redis中的key
        String key = RedisConstants.CACHE_SHOP_KEY + id;

        //通过查询缓存判断是否存在商品
        String shopJSON = stringRedisTemplate.opsForValue().get(key);    //实际上是RedisData类型数据
        //判断商品是否存在，不存在则查数据库并写入缓存
        //商品信息不存在，直接返回空
        if (StrUtil.isBlank(shopJSON)){
            return null;
        }

        //商品信息存在，判断是否过期
        RedisData redisData = JSONUtil.toBean(shopJSON, RedisData.class);
        //获取逻辑过期的时间，然后与当前时间进行比较
        LocalDateTime expireTime = redisData.getExpireTime();
        JSONObject data = (JSONObject) redisData.getData();
        R r = JSONUtil.toBean(data, type);
        if (expireTime.isAfter(LocalDateTime.now())){  //未过期
            //取出shop并返回
            return r;
        }
        //过期，进行缓存重建
        //尝试获取互斥锁
        String lock = RedisConstants.LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lock);
        //获取成功，开辟新线程重建缓存
        if (isLock){
            CACHE_REBUILD_EXCUTOR.submit(()->{  //通过submit方法提交redis重建任务
                try {
                    //重建缓存
                    //查询数据库
                    R r1 = dbFallBack.apply(id);
                    //写入redis
                    setWithLogicalExpire(key, r1, time, unit);
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    //释放锁
                    unLock(lock);
                }
            });
        }
        //获取失败or成功都返回旧数据
        return r;
    }


    /**
     *
     /**
     *
     * @param keyPrefix    key前缀
     * @param id    查询用的id
     * @param type  返回对象的类对象
     * @param dbFallBack    查询数据库的函数
     * @param time      正常数据的TTL
     * @param unit      正常数据的时间单位
     * @return
     * @param <R>       返回对象的类型
     * @param <ID>      id的类型
     */
    public <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallBack, Long time, TimeUnit unit){
        //构造redis中的key
        String key = keyPrefix + id;

        //通过查询缓存判断是否存在商品
        String shopJSON = stringRedisTemplate.opsForValue().get(key);

        //判断商品是否存在，不存在则查数据库并写入缓存
        //商品信息存在，反序列化为shop对象后返回
        if (StrUtil.isNotBlank(shopJSON)){  //只有当字符串有内容时才为真
            R r = JSONUtil.toBean(shopJSON, type);
            return r;
        }
        //商品信息缓存中不存在，查询数据库并写回redis，返回
        //判断缓存命中的是否为空值
        if (shopJSON != null){
            return null;
        }
        String lockKey = null;   //这是查询互斥锁的key
        R r = null;

        try {
            //未命中
            //尝试获取锁
            lockKey = RedisConstants.LOCK_SHOP_KEY + id;
            boolean isLock = tryLock(lockKey);
            //判断是否成功
            //失败，休眠并重试
            if (!isLock) {
                Thread.sleep(50);   //休眠
                queryWithMutex(keyPrefix, id, type, dbFallBack, time, unit);
            }
            //成功，查询数据库
            r = dbFallBack.apply(id);
            /*Thread.sleep(200);*/
            if (r == null) {
                //将空值写入redis，设置有效期为短有效期
                stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, unit);
                return null;
            }
            //存在数据，写入redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(r), time, unit); //将shop序列化为json存储
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //释放互斥锁
            unLock(lockKey);
        }
        return r;
    }
}
